package db.exchange.market;

import java.util.LinkedList;
import java.util.TreeMap;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingDeque;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import db.exchange.message.Message;
import db.exchange.message.MessageType;
import db.exchange.message.Operation;
import db.exchange.message.Order;

public class ExchangeEngine implements Runnable {

	private static final Logger LOG = LogManager.getLogger(ExchangeEngine.class.getName());

	private String symbol;
	private TreeMap<Double, LinkedList<Order>> sellStack = new TreeMap<>();
	private TreeMap<Double, LinkedList<Order>> buyStack = new TreeMap<>(
			new InverseDoubleComparator());
	public BlockingQueue<Message> outQueue = new LinkedBlockingDeque<>();
	public BlockingQueue<Order> inQueue = new LinkedBlockingDeque<>();

	public ExchangeEngine(String symbol) {
		this.symbol = symbol;
	}

	
	
	public String getEngineSymbol() {
		return symbol;
	}

	public void trade(Order order) {
		Message m = new Message(order, MessageType.ACK);
		outQueue.add(m); // send ACK
		while ((order.getLeavesQty() > 0) && matchOrder(order)) {
		}
		addOrderToStack(order);
		LOG.trace("place into stack: " + order);
	}

	private void addOrderToStack(Order order) {
		double price = order.getPrice();
		LinkedList<Order> list;
		switch (order.getOperation()) {
		case BUY:
			if (buyStack.containsKey(price)) {
				list = buyStack.get(price);
			} else {
				list = new LinkedList<>();
				buyStack.put(order.getPrice(), list);
			}
			list.add(order);
			break;
		case SELL:
			if (sellStack.containsKey(price)) {
				list = sellStack.get(price);
			} else {
				list = new LinkedList<>();
				sellStack.put(order.getPrice(), list);
			}
			list.add(order);
			break;
		default:
			LOG.error("wrong operation, clientId= " + order.getClientId());
		}
	}

	private boolean matchOrder(Order order) {
		double price = order.getPrice();
		TreeMap<Double, LinkedList<Order>> oppositeMap;

		if (order.getOperation() == Operation.BUY) {
			oppositeMap = sellStack;
			if (oppositeMap.isEmpty())
				return false;
			double oppositePrice = oppositeMap.firstKey();
			if (price >= oppositePrice) {
				processMatching(order, oppositeMap, oppositePrice);
				return true;
			} else {
				LOG.trace("No fill for order:" + order);
				return false;
			}

		} else if (order.getOperation() == Operation.SELL) {
			oppositeMap = buyStack;
			if (oppositeMap.isEmpty())
				return false;
			double oppositePrice = oppositeMap.firstKey();
			if (price <= oppositePrice) {
				processMatching(order, oppositeMap, oppositePrice);
				return true;
			} else {
				LOG.trace("No fill for order:" + order.getClientId());
				return false;
			}
		} else {
			LOG.error("wrong operation, clientId= " + order.getClientId());
			return false;
		}
	}

	private void processMatching(Order order,
			TreeMap<Double, LinkedList<Order>> oppositeMap, double oppositePrice) {
		LinkedList<Order> list;
		list = oppositeMap.get(oppositePrice);
		Order oppositeOrder = list.getFirst();
		LOG.trace("FILL: " + order + " : " + oppositeOrder);
		matchBothOrders (order, oppositeOrder);
		if (oppositeOrder.getLeavesQty() == 0)
			removeOrderFromStack(oppositeOrder, oppositeMap);
	}

	private void removeOrderFromStack(Order o, TreeMap<Double, LinkedList<Order>> map) {
		double price = o.getPrice();
		LinkedList<Order> list = map.get(price);
		list.removeFirst();
		if (list.isEmpty()) {
			map.remove(price);
		}
	}

	private void matchBothOrders (Order order, Order oppositeOrder) {
		int newQty = order.getLeavesQty();
		int oldQty = oppositeOrder.getLeavesQty();
		int fillQty = Math.min(newQty, oldQty);
		double fillPx = oppositeOrder.getPrice();
		order.fill(fillQty, fillPx, outQueue);
		oppositeOrder.fill(fillQty, fillPx, outQueue);
	}

	@Override
	public void run() {
		try {
			while (true) {
				Order order = inQueue.take();
				trade(order);
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
}